<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue 双向绑定原理</title>
    <script>

        /*
          第一步 实现基本架构
          第二步 M -> V 把模型的数据绑定到试图
          第三步 V -> M 把视图中用户输入的数据，触发事件更新模型中的数据，再更新视图
        */

        // 双向绑定源码实现
        // 发布者
        class Vue {
            constructor(options) {
                    this.options = options;
                    this.$data = options.data;
                    this.$el = document.querySelector(options.el)

                    this._directives = {} // 存放订阅者
                        //{myText:[订阅者1 ,订阅者2],myTBox:[订阅者1]}

                    this.Observer(this.$data); // 传递数据
                    this.Compile(this.$el); // 传递顶层DOM对象 #app
                }
                // 劫持数据
            Observer(data) {
                    // 更新视图，局部更新
                    for (let key in data) {
                        this._directives[key] = [];
                        let Val = data[key]; //当前值;
                        let _this = this._directives[key];

                        Object.defineProperty(this.$data, key, {
                            get: function() {
                                return Val;
                            },
                            set: function(newVal) {
                                if (newVal !== Val) {
                                    Val = newVal;
                                    _this.forEach(watcher => { // 遍历订阅者实例
                                        watcher.update(); // 更新视图
                                    });
                                }
                            }
                        })
                    }
                }
                // 解析指令
            Compile(el) {
                let nodes = el.children; // 获取App下的子元素
                for (let i = 0; i < nodes.length; i++) {
                    let node = nodes[i]; // 当前元素
                    if (node.children.length) { // 递归遍历子元素
                        this.Compile(node);
                    }
                    if (node.hasAttribute('v-text')) { // 查找v-text
                        let attVal = node.getAttribute('v-text');
                        this._directives[attVal].push(new Watcher(node, attVal, this, "innerHTML")); // 存放订阅者的实例对象
                    }
                    if (node.hasAttribute('v-model')) {
                        let attVal = node.getAttribute('v-model');
                        this._directives[attVal].push(new Watcher(node, attVal, this, "value")); // 存放订阅者的实例对象
                        let _this = this;
                        // 这里不能用 change 事件 用change事件修改数据后在失去焦点时才会触发
                        // input 是实时触发
                        /* 闭包版本
                        node.addEventListener('change', (function() {
                            return function() {
                                _this.$data[attVal] = node.value; // 赋值触发set
                            }
                        })())
                        */

                        // 用箭头函数简化代码
                        node.addEventListener('input', () => {
                          _this.$data[attVal] = node.value; // 赋值触发set
                        });

                    }
                }
            }
        }

        // 订阅者
        class Watcher {
            constructor(el, vm, mySelf, attr) {
                this.el = el;
                this.vm = vm;
                this.mySelf = mySelf;
                this.attr = attr;
                this.update() // 初始化数据

            }
            update() {
                this.el[this.attr] = this.mySelf.$data[this.vm];
            }
        }
    </script>
</head>

<body>
    <div id="app">
        <h1>双向数据绑定实现</h1>
        <div>
            <div v-text="myText"></div>
            <input type="text" v-model="myText">
        </div>
    </div>
    <script>
        // 使用
        const vm = new Vue({
            el: '#app',
            data: {
                myText: 'hello lynn!'
            }
        })
    </script>
</body>

</html>